
package todolist.entity;

import java.util.Date;
import java.util.GregorianCalendar;
import todolist.personalization.DateTimeDescriptionCustomizer;
import todolist.personalization.PriorityCustomizer;
import todolist.personalization.ReminderOffsetCustomizer;
import todolist.personalization.TaskFieldCustomizer;
import todolist.personalization.TaskStatusCustomizer;
import todolist.util.Utils;

/**
 * Abstraction of a Task.
 * Validation logic handled separately.
 * @author Ivan
 */
public class TaskEntity implements Comparable<TaskEntity>, Cloneable {
	
	public static final int NO_REMINDER_OFFSET = Integer.MIN_VALUE;
	
	private long _lngTaskId; // readonly
	private String _strTaskName; // single line, never null or ""
	private DateTimeEntity _entStartDT;
	private DateTimeEntity _entEndDT;
	private PriorityType _enmPriority; // never null
	private String _strTaskComments; // single line, never null
	private Date _dtmReminderDate;
	private TaskStatusType _enmTaskStatus;
	
	// constructors
	
	/**
	 * Creates a new Task.
	 * Care should be taken to ensure TaskId does not already exist.
	 */
	public TaskEntity(long plngTaskId, String pstrTaskName) {
		_lngTaskId = plngTaskId;
		setTaskName(pstrTaskName);
		_entStartDT = null;
		_entEndDT = null;
		setPriority(PriorityCustomizer.getInstance().getDefaultPriorityType());
		setTaskComments("");
		_dtmReminderDate = null; // no reminder by default
		setTaskStatus(TaskStatusCustomizer.getInstance().getDefaultTaskStatus());
	}
	
	/**
	 * Creates a Task from another.
	 */
	public TaskEntity(TaskEntity pentTask) {
		_lngTaskId = pentTask.getTaskId();
		_strTaskName = pentTask.getTaskName();
		_entStartDT = pentTask.getStartDT();
		_entEndDT = pentTask.getEndDT();
		_enmPriority = pentTask.getPriority();
		_strTaskComments = pentTask.getTaskComments();
		_dtmReminderDate = pentTask.getReminderDate();
		_enmTaskStatus = pentTask.getTaskStatus();
	}
	
	// getters

	/**
	 * Task ID, unique identifier.
	 */
	public long getTaskId() {
		return _lngTaskId;
	}

	/**
	 * Mandatory Task name, should be single line.
	 */
	public String getTaskName() {
		return _strTaskName;
	}

	/**
	 * Start Date/Time of the Task. May be null.
	 */
	public DateTimeEntity getStartDT() {
		return _entStartDT;
	}

	/**
	 * End Date/Time of the Task. May be null.
	 */
	public DateTimeEntity getEndDT() {
		return _entEndDT;
	}
	
	/**
	 * Priority/urgency of Task.
	 */
	public PriorityType getPriority() {
		return _enmPriority;
	}

	/**
	 * Optional single line comments.
	 * Should never be null.
	 */
	public String getTaskComments() {
		return _strTaskComments;
	}

	/**
	 * Reminder Date if exists, or else null.
	 */
	public Date getReminderDate() {
		return _dtmReminderDate;
	}
	
	/**
	 * Status of Task. It is not the responsibility of this attribute to interpret its status based on DT info.
	 */
	public TaskStatusType getTaskStatus() {
		return _enmTaskStatus;
	}
	
	// setters

	/**
	 * Mandatory Task name, must be single line.
	 * null input, empty String or multiline input will result in IllegalArgumentException being thrown.
	 * @throws IllegalArgumentException
	 */
	public void setTaskName(String pstrTaskName) throws IllegalArgumentException {
		if (pstrTaskName == null || pstrTaskName.equals(""))
			throw new IllegalArgumentException("TaskEntity.setTaskName(): Task Name is mandatory.");
		if (pstrTaskName.contains(Utils.getLineSeparator()))
			throw new IllegalArgumentException("TaskEntity.setTaskName(): Task Name must be single line.");
		this._strTaskName = pstrTaskName;
	}

	/**
	 * Start Date/Time of the Task. May be null.
	 * If both Start Date & End Date exist and are strict, Start Date should be before or equal to the End Date.
	 * @throws IllegalArgumentException
	 */
	public void setStartDT(DateTimeEntity pentStartDT) throws IllegalArgumentException {
		checkDateSequence(pentStartDT, _entEndDT); // may throw IllegalArgumentException
		this._entStartDT = pentStartDT;
	}

	/**
	 * End Date/Time of the Task. May be null.
	 * Automatically adjusts Reminder Date so that the Reminder offset remains the same, or to null if End/Reminder Date/Time is null.
	 * If both Start Date & End Date exist and are strict, Start Date should be before or equal to the End Date.
	 * @throws IllegalArgumentException
	 */
	public void setEndDT(DateTimeEntity pentEndDT) throws IllegalArgumentException {
		checkDateSequence(_entStartDT, pentEndDT); // may throw IllegalArgumentException
		int intOffset = getReminderOffset(); // calculate offset, negative if no reminder is to be set
		_entEndDT = pentEndDT;
		setReminderOffset(intOffset); // calculate Reminder DT from End DT
	}
	
	/**
	 * Priority/urgency of Task.
	 * @throws IllegalArgumentException
	 */
	public void setPriority(PriorityType penmPriority) throws IllegalArgumentException {
		if (penmPriority == null)
			throw new IllegalArgumentException("TaskEntity.setPriority(): Priority cannot be null.");
		this._enmPriority = penmPriority;
	}

	/**
	 * Optional comments, should not be multiline or null.
	 * null input will be replaced by empty String.
	 * multiline input will be result in IllegalArgumentException being thrown.
	 * @throws IllegalArgumentException
	 */
	public void setTaskComments(String pstrTaskComments) throws IllegalArgumentException {
		if (pstrTaskComments == null)
			pstrTaskComments = "";
		if (pstrTaskComments.contains(Utils.getLineSeparator()))
			throw new IllegalArgumentException("TaskEntity.setTaskComments(): Task Comments must be single line.");
		this._strTaskComments = pstrTaskComments;
	}

	/**
	 * Status of Task. It is not the responsibility of this attribute to interpret its status based on DT info.
	 * @throws IllegalArgumentException
	 */
	public void setTaskStatus(TaskStatusType penmTaskStatus) throws IllegalArgumentException {
		if (penmTaskStatus == null)
			throw new IllegalArgumentException("TaskEntity.setTaskStatus(): Task Status cannot be null.");
		this._enmTaskStatus = penmTaskStatus;
	}
	
	// String conversion facilitators

	/**
	 * String representation of Date/Time Description, both Start and End if any, for this Task.
	 * Empty string if non-temporal.
	 */
	public String getDTString() {
		String strStartDT = (_entStartDT == null ? null : _entStartDT.toString());
		String strEndDT = (_entEndDT == null ? null : _entEndDT.toString());
		if (strStartDT == null && strEndDT == null)
			return "null";
		else if (strStartDT != null && strEndDT != null)
			return
				DateTimeDescriptionCustomizer.getInstance().getDTDisplayBothStartDate() + " " + strStartDT + " " + 
				DateTimeDescriptionCustomizer.getInstance().getDTDisplayBothEndDate() + " " + strEndDT;
		else if (strStartDT != null)
			return DateTimeDescriptionCustomizer.getInstance().getDTDisplayOneStartDate() + " " + strStartDT;
		else if (strEndDT != null)
			return DateTimeDescriptionCustomizer.getInstance().getDTDisplayOneEndDate() + " " + strEndDT;
		else // shouldn't happen =X
			throw new RuntimeException("TaskEntity.getDTString(): Logic error.");
	}

	/**
	 * Returns String representation of this Task's Priority Type.
	 */
	public String getPriorityString() {
		switch (_enmPriority) {
			case PRIORITY_HIGH:
				return PriorityCustomizer.getInstance().getDisplayHighPriority();
			case PRIORITY_MID:
				return PriorityCustomizer.getInstance().getDisplayMidPriority();
			case PRIORITY_LOW:
				return PriorityCustomizer.getInstance().getDisplayLowPriority();
			default: // shouldn't happen =X
				throw new IllegalArgumentException("TaskEntity.getPriorityString(): Invalid Priority Type.");
		}
	}

	/**
	 * Returns String representation of this Task's Reminder Offset.
	 */
	public String getReminderString() {
		int intReminderOffset = getReminderOffset();
		if (intReminderOffset < 0)
			intReminderOffset = NO_REMINDER_OFFSET;
		String strRO = ReminderOffsetCustomizer.getInstance().findDisplayDescriptorByValue(intReminderOffset);
		assert strRO != null; // check for improper customisation of reminder offset display values
		return strRO;
	}

	/**
	 * Returns String representation of this Task's Status.
	 */
	public String getTaskStatusString() {
		switch (_enmTaskStatus) {
			case COMPLETED:
				return TaskStatusCustomizer.getInstance().getDisplayTaskStatusCompleted();
			case PENDING:
				return TaskStatusCustomizer.getInstance().getDisplayTaskStatusPending();
			case IN_PROGRESS:
				return TaskStatusCustomizer.getInstance().getDisplayTaskStatusInProgress();
			case OVERDUE:
				return TaskStatusCustomizer.getInstance().getDisplayTaskStatusOverdue();
			default: // shouldn't happen =X
				throw new IllegalArgumentException("TaskEntity.getTaskStatusString(): Invalid Task Status.");
		}
	}
	
	// other facilitators
	
	/**
	 * Checks whether a given Start Date and End Date are valid.
	 * If both Start Date & End Date exist and are strict,
	 *	then Start Date should be before or equal to the End Date.
	 */
	public static boolean validateDateSequence(DateTimeEntity pentStartDT, DateTimeEntity pentEndDT) {
		if (pentStartDT == null || pentEndDT == null ||
			!pentStartDT.isStrict() || !pentEndDT.isStrict())
			return true;
		if (pentEndDT.getInternalDate().before(pentStartDT.getInternalDate()))
			return false;
		return true;
	}

	/**
	 * Short description of this Task, similar to toString().
	 */
	public String toShortString() {
		return
			_lngTaskId + " " + getPriorityString() + " " + _strTaskName + " " +
			(_entEndDT == null ? "null" : _entEndDT.toString());
	}
	
	/**
	 * Sets both Start and End Date/Time.
	 * Used to prevent IllegalArgumentException being inadvertently thrown during Date sequence validation
	 *	when setting Date/Time one at a time.
	 * @throws IllegalArgumentException
	 */
	public void setBothDT(DateTimeEntity pentStartDT, DateTimeEntity pentEndDT) throws IllegalArgumentException {
		checkDateSequence(pentStartDT, pentEndDT); // may throw IllegalArgumentException
		setStartDT(null);
		setEndDT(pentEndDT);
		setStartDT(pentStartDT);
	}

	/**
	 * Number of min that Reminder Date/Time is before End Date/Time, or negative value for no reminder.
	 */
	public void setReminderOffset(int pintOffset) {
		if (pintOffset < 0 || _entEndDT == null) {
			_dtmReminderDate = null;
			return;
		}
		// calculate Reminder DT from End DT
		GregorianCalendar objGC = new GregorianCalendar();
		objGC.setLenient(false);
		objGC.setTime(_entEndDT.getInternalDate());
		objGC.add(GregorianCalendar.MINUTE, -pintOffset);
		_dtmReminderDate = objGC.getTime();
	}

	// private methods
	
	/**
	 * Checks whether Start Date and End Date are valid.
	 * @throws IllegalArgumentException
	 */
	private void checkDateSequence(DateTimeEntity pentStartDT, DateTimeEntity pentEndDT) throws IllegalArgumentException {
		if (!validateDateSequence(pentStartDT, pentEndDT))
			throw new IllegalArgumentException("TaskEntity.checkDateSequence(): Strict Start Date has to be <= Strict End Date.");
	}

	/**
	 * Number of min that Reminder Date/Time is before End Date/Time, or -ve if no Reminder DT, End DT, or Reminder DT has passed.
	 * Assumes offset is within int32 range.
	 */
	private int getReminderOffset() {
		if (_entEndDT == null || _dtmReminderDate == null)
			return Integer.MIN_VALUE;
		return (int) (_entEndDT.getInternalDate().getTime() - _dtmReminderDate.getTime()) / 60000; // 60000 ms in a min
	}
	
	// overrides

	/**
	 * Compares equality by Task ID.
	 */
	@Override
	public boolean equals(Object pobjOther) {
		if (pobjOther == null || !(pobjOther instanceof TaskEntity))
			return false;
		TaskEntity entOther = (TaskEntity) pobjOther;
		return _lngTaskId == entOther.getTaskId();
	}

	/**
	 * Compares by Task ID.
	 * Input expected to be non-null. Assumes subtraction of Task ID will not overflow when cast to int32.
	 */
	@Override
	public int compareTo(TaskEntity pentOther) {
		return (int) (_lngTaskId - pentOther.getTaskId());
	}

	/**
	 * Cloning is supported.
	 */
	@Override
	public Object clone() {
		return new TaskEntity(this);
	}
	
	/**
	 * Long description of this Task.
	 */
	@Override
	public String toString() {
		StringBuffer objSB = new StringBuffer(1000);
		// append Task ID
		objSB.append(TaskFieldCustomizer.getInstance().getDisplayFieldTaskID());
		objSB.append(TaskFieldCustomizer.getInstance().getDisplayFieldDelimiter());
		objSB.append(" ");
		objSB.append(_lngTaskId);
		objSB.append(Utils.getLineSeparator());
		// append Task Name
		objSB.append(TaskFieldCustomizer.getInstance().getDisplayFieldTaskName());
		objSB.append(TaskFieldCustomizer.getInstance().getDisplayFieldDelimiter());
		objSB.append(" ");
		objSB.append(_strTaskName);
		objSB.append(Utils.getLineSeparator());
		// append DT
		objSB.append(TaskFieldCustomizer.getInstance().getDisplayFieldDT());
		objSB.append(TaskFieldCustomizer.getInstance().getDisplayFieldDelimiter());
		objSB.append(" ");
		objSB.append(getDTString());
		objSB.append(Utils.getLineSeparator());
		// append Priority
		objSB.append(TaskFieldCustomizer.getInstance().getDisplayFieldPriority());
		objSB.append(TaskFieldCustomizer.getInstance().getDisplayFieldDelimiter());
		objSB.append(" ");
		objSB.append(getPriorityString());
		objSB.append(Utils.getLineSeparator());
		// append Comments
		objSB.append(TaskFieldCustomizer.getInstance().getDisplayFieldTaskComments());
		objSB.append(TaskFieldCustomizer.getInstance().getDisplayFieldDelimiter());
		objSB.append(" ");
		objSB.append(_strTaskComments);
		objSB.append(Utils.getLineSeparator());
		// append Reminder Offset
		objSB.append(TaskFieldCustomizer.getInstance().getDisplayFieldReminder());
		objSB.append(TaskFieldCustomizer.getInstance().getDisplayFieldDelimiter());
		objSB.append(" ");
		objSB.append(getReminderString());
		objSB.append(Utils.getLineSeparator());
		// TODO : v0.x - append Status
		/*
		objSB.append(TaskFieldCustomizer.getInstance().getDisplayFieldStatus());
		objSB.append(TaskFieldCustomizer.getInstance().getDisplayFieldDelimiter());
		objSB.append(" ");
		objSB.append(getTaskStatusString());
		objSB.append(Utils.getLineSeparator()); */
		return objSB.toString();
	}
	
}
